// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: daniel

#pragma once

#include <deque>
#include <memory>
#include <string>

#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <glog/logging.h>

#include "coin2/base/request/http2/asio_context.h"
#include "coin2/base/request/http2/error.h"
#include "coin2/base/request/http2/request_parser.h"
#include "coin2/base/time.h"
#include "nghttp2/includes/nghttp2/asio_http2_client.h"

namespace coin2::base::request::http2 {

class Connection : public std::enable_shared_from_this<Connection> {
  using NgHttp2Session = nghttp2::asio_http2::client::session;
  using NgHttp2Response = nghttp2::asio_http2::client::response;
  using Http2AsioContext = coin2::base::request::http2::AsioContext;
  using CallbackType =
      std::function<void(std::shared_ptr<Http2AsioContext>, const boost::system::error_code&)>;

 public:
  explicit Connection(
      boost::asio::io_context* io_ctx,
      boost::asio::ssl::context* tls_ctx,
      const std::string& host,
      int port,
      const std::string& bind_ip);
  ~Connection();
  Connection(const Connection&) = delete;
  Connection& operator=(const Connection&) = delete;

  void Submit(std::weak_ptr<Http2AsioContext> context, CallbackType callback);
  void Connect();
  void Suspend();

  bool IsConnected() const { return is_connected_; }
  bool IsCompleted() const { return request_sent_ == request_recv_; }
  auto RequestSent() const -> int64_t { return request_sent_; }
  auto RequestRecv() const -> int64_t { return request_recv_; }

  auto GetRequestsLimit() const { return request_limit_; }
  void SetRequestsLimit(int64_t request_limit) { request_limit_ = request_limit; }
  bool HitRequestsLimit() const { return request_limit_ > 0 && request_sent_ >= request_limit_; }
  bool ConnectionExpired(int64_t ts) const {
    // based on binance, the connection is available for 1 hour
    return conn_start_ts_ + 3300'000'000'000LL /* 55 mins */ <= ts;
  }

  auto GetFailTolerance() const { return fail_tolerance_; }
  void SetFailTolerance(int64_t fail_tolerance) { fail_tolerance_ = fail_tolerance; }
  bool HitFailTolerance() const { return fail_tolerance_ > 0 && hit_fail_tolerance_; }

  int64_t ConnectionID() const { return conn_id_debug_; }

  template <typename Actual, typename Expect>
  void OnFail(const Actual& actual, const Expect& expect) {
    if (actual == expect) {
      return;
    }
    if (!IsConnected() || fail_tolerance_ == 0 || HitFailTolerance()) {
      return;
    }
    send_fail_queue_.push_back(GetCurrentTimestamp());
    if (send_fail_queue_.size() < fail_tolerance_) {
      return;
    }
    while (send_fail_queue_.size() > fail_tolerance_) {
      send_fail_queue_.pop_front();
    }
    auto first = send_fail_queue_.front();
    auto last = send_fail_queue_.back();
    if (last - first > one_minutes_ns_) {
      return;
    }
    send_fail_queue_.clear();
    hit_fail_tolerance_ = true;
    LOG(ERROR) << "[http2] send fail tolerance hit[" << fail_tolerance_ << "]";
  }

 private:
  void InitSsl();
  void DoConnect();
  void ConnectWithSsl();
  void ConnectWithNonSsl();
  void Reconnect();
  void Shutdown();
  void OnConnect(boost::asio::ip::tcp::resolver::iterator endpoint_it);
  void OnError(const boost::system::error_code& ec);

  void OnResponse(std::weak_ptr<Http2AsioContext> context, const NgHttp2Response& res);
  void OnData(std::weak_ptr<Http2AsioContext> context, const uint8_t* data, std::size_t len);
  void OnClose(std::weak_ptr<Http2AsioContext> context, CallbackType callback, uint32_t error_code);

  bool IsConnecting() {
    if (is_connecting_) {
      return true;
    }
    is_connecting_ = true;
    return false;
  }
  void IsConnecting(bool done) { is_connecting_ = done; }

 private:
  boost::asio::io_context* io_ctx_;
  boost::asio::ssl::context* tls_ctx_;
  const std::string host_;
  const int port_;
  const std::string bind_ip_;

  boost::asio::deadline_timer deadline_timer_;
  std::unique_ptr<NgHttp2Session> session_;

  bool use_ssl_ = false;
  bool is_connected_ = false;
  bool is_connecting_ = false;

  int reconnect_interval_ = 1;  // 1 seconds

  int64_t request_sent_ = 0;
  int64_t request_recv_ = 0;
  int64_t request_limit_ = 0;

  bool hit_fail_tolerance_ = false;
  int64_t fail_tolerance_ = 0;
  std::deque<int64_t> send_fail_queue_;
  const int64_t one_minutes_ns_ = 60 * 1000'000'000LL;  // 1 minutes

  int64_t conn_start_ts_ = 0;
  int64_t conn_id_debug_ = 0;
};

}  // namespace coin2::base::request::http2
